/* @flow */

import config from '../config';
import { warn } from './debug';
import { set } from '../observer/index';
import { unicodeRegExp } from './lang';
import { nativeWatch, hasSymbol } from './env';

import { ASSET_TYPES, LIFECYCLE_HOOKS } from 'shared/constants';

import {
  extend,
  hasOwn,
  camelize,
  toRawType,
  capitalize,
  isBuiltInTag,
  isPlainObject,
} from 'shared/util';

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */
const strats = config.optionMergeStrategies;

/**
 * el 的合并
 * el 提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标，
 * 因此它只在创建Vue实例才存在，在子类或者子组件中无法定义el选项，
 * 因此el的合并策略是在保证选项只存在于根的Vue实例的情形下使用默认策略进行合并
 */
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      // 只允许vue实例才拥有el属性，其他子类构造器不允许有el属性
      warn(
        `option "${key}" can only be used during instance ` +
          'creation with the `new` keyword.'
      );
    }
    // 默认策略
    return defaultStrat(parent, child);
  };
}

/**
 * Helper that recursively merges two data objects together.
 */
function mergeData(to: Object, from: ?Object): Object {
  if (!from) return to;
  let key, toVal, fromVal;

  const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from);

  for (let i = 0; i < keys.length; i++) {
    key = keys[i];
    // in case the object is already observed...
    if (key === '__ob__') continue;
    toVal = to[key];
    fromVal = from[key];
    if (!hasOwn(to, key)) {
      // 子的数据父没有，则将新增的数据加入响应式系统中。
      set(to, key, fromVal);
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      // 处理深层对象，当合并的数据为多层嵌套对象时，需要递归调用 mergeData 进行比较合并
      mergeData(toVal, fromVal);
    }
  }
  return to;
}

/**
 * data策略最终调用的mergeDataOrFn方法
 */
export function mergeDataOrFn(
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // 子类不存在data选项，则合并结果为父类data选项
    if (!childVal) {
      return parentVal;
    }
    if (!parentVal) {
      // 父类不存在data选项，则合并结果为子类data选项
      return childVal;
    }
    // data 选项在父类和子类同时存在的情况下返回的是一个函数
    return function mergedDataFn() {
      // 子类实例和父类实例，分别将子类和父类实例中 data 函数执行后返回的对象传递给mergeData函数做数据合并
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      );
    };
  } else {
    // Vue实例
    // vue构造函数实例对象
    return function mergedInstanceDataFn() {
      // instance merge
      const instanceData =
        typeof childVal === 'function' ? childVal.call(vm, vm) : childVal;
      const defaultData =
        typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal;
      if (instanceData) {
        // 当实例中传递data选项时，将实例的data对象和Vm构造函数上的data属性选项合并
        return mergeData(instanceData, defaultData);
      } else {
        // 当实例中不传递data时，默认返回Vm构造函数上的data属性选项
        return defaultData;
      }
    };
  }
}

// data 的合并 parentVal 是 父组件的 data，childVal 是 子组件的 data ，vm 是组件实例
strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  // vm 代表是否为 Vue 创建的实例，否则是子父类的关系

  // console.log('====parentVal=====================');
  // console.log(parentVal);
  // console.log('====================================');

  // console.log('  ');

  // console.log('====childVal=====================');
  // console.log(childVal);
  // console.log('====================================');

  // console.log('                ');
  // console.log('====vm=====================');
  // console.log(vm);
  // console.log('====================================');

  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' &&
        warn(
          'The "data" option should be a function ' +
            'that returns a per-instance value in component ' +
            'definitions.',
          vm
        );

      return parentVal;
    }
    return mergeDataOrFn(parentVal, childVal);
  }

  return mergeDataOrFn(parentVal, childVal, vm);
};

/**
 * 生命周期钩子选项合并策略 合并为一个函数
 */
function mergeHook(
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  // 1.如果子类和父类都拥有钩子选项，则将子类选项和父类选项合并,
  // 2.如果父类不存在钩子选项，子类存在时，则以数组形式返回子类钩子选项，
  // 3.当子类不存在钩子选项时，则以父类选项返回。
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
      ? childVal
      : [childVal]
    : parentVal;
  return res ? dedupeHooks(res) : res;
}

// 防止多个组件实例钩子选项相互影响
function dedupeHooks(hooks) {
  const res = [];
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i]);
    }
  }
  return res;
}

/**
 * // 对生命周期钩子选项的合并都执行mergeHook策略
 */
LIFECYCLE_HOOKS.forEach((hook) => {
  strats[hook] = mergeHook;
});

/**
 * 资源选项自定义合并策略
 */
function mergeAssets(
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null);
  if (childVal) {
    process.env.NODE_ENV !== 'production' &&
      assertObjectType(key, childVal, vm);
    return extend(res, childVal);
  } else {
    return res;
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets;
});

/**
 * watch 的合并
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // 火狐浏览器在Object的原型上拥有watch方法，这里对这一现象做了兼容
  // var nativeWatch = ({}).watch;
  if (parentVal === nativeWatch) parentVal = undefined;
  if (childVal === nativeWatch) childVal = undefined;

  // 没有子，则默认用父选项
  if (!childVal) return Object.create(parentVal || null);

  // 保证watch选项是一个对象
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm);
  }

  // 没有父则直接用子选项
  if (!parentVal) return childVal;

  const ret = {};

  extend(ret, parentVal);

  for (const key in childVal) {
    let parent = ret[key];
    const child = childVal[key];
    // 父的选项先转换成数组
    if (parent && !Array.isArray(parent)) {
      parent = [parent];
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child)
      ? child
      : [child];
  }
  return ret;
};

/**
 * Other object hashes.
 */
strats.props = strats.methods = strats.inject = strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm);
  }
  if (!parentVal) return childVal;
  const ret = Object.create(null);
  extend(ret, parentVal);
  if (childVal) extend(ret, childVal);
  return ret;
};
strats.provide = mergeDataOrFn;

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal;
};

/**
 * components 规范检验
 */
function checkComponents(options: Object) {
  // 遍历components对象，对每个属性值校验。
  for (const key in options.components) {
    validateComponentName(key);
  }
}

/**
 * 校验 component name
 * @param {*} name
 */
export function validateComponentName(name: string) {
  if (
    // 正则判断检测是否为非法的标签，例如数字开头
    !new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)
  ) {
    warn(
      'Invalid component name: "' +
        name +
        '". Component names ' +
        'should conform to valid custom element name in html5 specification.'
    );
  }

  // 不能使用Vue自身自定义的组件名，如slot, component,不能使用html的保留标签，如 h1, svg等
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
        'id: ' +
        name
    );
  }
}

/**
 *  props 规范校验
 */
function normalizeProps(options: Object, vm: ?Component) {
  const props = options.props;
  if (!props) return;
  const res = {};
  let i, val, name;
  // props选项数据有两种形式，一种是props:['a', 'b', 'c'],一种是props:{ a: { type: 'String', default: 'hahah' }}
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === 'string') {
        name = camelize(val);

        // 默认将数组形式的props转换为对象形式。
        res[name] = { type: null };
      } else if (process.env.NODE_ENV !== 'production') {
        // 规则：当使用数组props是，内部元素必须保证是字符串
        warn('props must be strings when using array syntax.');
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val) ? val : { type: val };
    }
  } else if (process.env.NODE_ENV !== 'production') {
    // 非数组，非对象则判定props选项传递非法
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
        `but got ${toRawType(props)}.`,
      vm
    );
  }
  options.props = res;
}

/**
 * inject 的规范校验
 */
function normalizeInject(options: Object, vm: ?Component) {
  const inject = options.inject;
  if (!inject) return;
  /**
   * // 数组写法
   *inject: ['foo'],

    // 对象写法
    inject: {
      foo: {
        from: 'foo',
        default: 'bardefault'
      }
    }
   *
   */
  const normalized = (options.inject = {});
  if (Array.isArray(inject)) {
    // 数组的处理规则
    // from: 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] };
    }
  } else if (isPlainObject(inject)) {
    // 对象的处理
    for (const key in inject) {
      const val = inject[key];
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val };
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
        `but got ${toRawType(inject)}.`,
      vm
    );
  }
}

/**
 *  directive 的规范校验
 */
function normalizeDirectives(options: Object) {
  const dirs = options.directives;

  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key];
      /**
       * 指令函数简写方式
       *  directives: {
            'color-swatch': function(el, binding) {
                el.style.backgroundColor = binding.value
            }
          }
       *
       *
       */
      if (typeof def === 'function') {
        // 函数简写同样会转换成对象的形式
        dirs[key] = { bind: def, update: def };
      }
    }
  }
}

function assertObjectType(name: string, value: any, vm: ?Component) {
  if (!isPlainObject(value)) {
    warn(
      `Invalid value for option "${name}": expected an Object, ` +
        `but got ${toRawType(value)}.`,
      vm
    );
  }
}

/**
 * 合并 options
 */
export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // console.log(child);
  if (process.env.NODE_ENV !== 'production') {
    // components 规范检验
    checkComponents(child);
  }

  if (typeof child === 'function') {
    child = child.options;
  }

  // props 的校验和规范化
  normalizeProps(child, vm);

  // inject 的校验和规范化
  normalizeInject(child, vm);

  //directives 的校验和规范化
  normalizeDirectives(child);

  // 针对extends扩展的子类构造器
  if (!child._base) {
    // extends
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm);
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm);
      }
    }
  }

  const options = {};
  let key;
  for (key in parent) {
    mergeField(key);
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  function mergeField(key) {
    /**
     * 1. 用 data,props,computed等选项定义实例数据
     * 2. 用 mounted, created, destoryed 等定义生命周期函数
     * 3. 用 components 注册组件
     * 4. 用 methods 选项定义实例方法
     */
    // 拿到各个选择指定的选项配置，如果没有则用默认的配置
    const strat = strats[key] || defaultStrat;

    // 执行各自的合并策略，其中包括 methods props data 等的分别合并策略
    options[key] = strat(parent[key], child[key], vm, key);
  }
  // console.log(options);
  return options;
}

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 * 拿到组件选项中的具体配置项
 */
export function resolveAsset(
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return;
  }
  const assets = options[type];
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id];
  const camelizedId = camelize(id);
  if (hasOwn(assets, camelizedId)) return assets[camelizedId];
  const PascalCaseId = capitalize(camelizedId);
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId];
  // fallback to prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
  }
  return res;
}
